package org.jing.ext.extra;

import org.jing.core.lang.JingException;
import org.jing.core.util.GenericUtil;
import org.jing.ext.stream.StreamUtil;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.util.List;

public class CharsetDetector {
    private int bufferSize = 1024 * 3;

    private int limit = 3;

    private final List<String> testCharsets = GenericUtil.<String>buildList()
        .add("UTF-8")
        .add("GBK")
        .add("ISO-8859-1")
        .get();

    public int getBufferSize() {
        return bufferSize;
    }

    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        this.limit = limit;
    }

    public List<String> getTestCharsets() {
        return testCharsets;
    }

    public String detectBuffer(byte[] buffer) {
        try (
            ByteArrayInputStream bis4Bom = new ByteArrayInputStream(buffer);
            ByteArrayInputStream bis4Valid = new ByteArrayInputStream(buffer)
        ) {
            return detect(bis4Bom, bis4Valid);
        }
        catch (JingException e) {
            throw e;
        }
        catch (Exception e) {
            throw new JingException(e, "failed to detect charset");
        }
    }

    public String detectStream(InputStream is) {
        byte[] buffer = StreamUtil.getBuffer(is);
        return detectBuffer(buffer);
    }

    public String detectFile(File file) {
        try (
            FileInputStream fis = new FileInputStream(file)
        ) {
            return detectStream(fis);
        }
        catch (JingException e) {
            throw e;
        }
        catch (Exception e) {
            throw new JingException(e, "failed to detect charset");
        }
    }

    private String detect(InputStream is4Bom, InputStream is4Encoding) throws Exception {
        // 1. 优先检查 BOM 标记
        String bomEncoding = validBOM(is4Bom);
        if (bomEncoding != null)
            return bomEncoding;

        // 2. 尝试常见编码的严格模式
        for (String encoding : testCharsets) {
            if (validEncoding(is4Encoding, encoding)) {
                return encoding;
            }
        }

        // 3. 报错
        throw new JingException("cannot detect charset");
    }

    private String validBOM(InputStream is) throws IOException {
        byte[] bom = new byte[3];
        int read = is.read(bom);

        // UTF-8 BOM: EF BB BF
        if (read >= 3 && (bom[0] & 0xFF) == 0xEF && (bom[1] & 0xFF) == 0xBB && (bom[2] & 0xFF) == 0xBF) {
            return "UTF-8";
        }
        // UTF-16 Big-Endian BOM: FE FF
        else if (read >= 2 && (bom[0] & 0xFF) == 0xFE && (bom[1] & 0xFF) == 0xFF) {
            return "UTF-16BE";
        }
        // UTF-16 Little-Endian BOM: FF FE
        else if (read >= 2 && (bom[0] & 0xFF) == 0xFF && (bom[1] & 0xFF) == 0xFE) {
            return "UTF-16LE";
        }
        return null;
    }

    private boolean validEncoding(InputStream is, String encoding) throws IOException {
        CharsetDecoder decoder = Charset.forName(encoding).newDecoder()
            .onMalformedInput(CodingErrorAction.REPORT)   // 遇到错误字节时报错
            .onUnmappableCharacter(CodingErrorAction.REPORT);

        try (InputStreamReader isr = new InputStreamReader(is, decoder)) {
            char[] buffer = new char[1024 * 3];
            for (int i$ = 0; i$ < limit; i$ ++) {
                if (isr.read(buffer) == -1) {
                    break;
                }
            }
            return true;
        } catch (MalformedInputException | UnmappableCharacterException e) {
            return false;
        }
    }
}
